﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using storeProcedure;

namespace Player
{
    public enum PlayerType
    {
        Unknown = -1,
        WindowsPlayer = 0,
        UnixPlayer = 1
    }

    public enum SessionDataStatus
    {
        DataIsCompleted,
        SomeDataIsMissing,
        NoDataIsCapturedForThisSessionYet,
        DataNotFound,
        NoAuditStoreDatabaseAvailable,
        MultipleAuditStoreAvailable
    }

    public class AuditStore
    {
        public int Id { get; protected set; }
        public Guid Guid { get; protected set; }
        public string Name { get; protected set; }
        public bool TrustedAgentEnabled { get; protected set; }
        public bool TrustedCollectorEnabled { get; protected set; }
        public int ActiveDatabaseId { get; protected set; }

        public AuditStore(AuditStoreList.Output output)
        {
            Id = output.Id;
            Guid = output.Guid;
            Name = output.Name;
            TrustedAgentEnabled = output.TrustedAgentEnabled;
            TrustedCollectorEnabled = output.TrustedCollectorEnabled;
            ActiveDatabaseId = output.ActiveDatabaseId;
        }
    }

    class AuditStoreDatabase
    {
        public int Id { get; protected set; }
        public int AuditStoreId { get; protected set; }
        public Guid Guid { get; protected set; }
        public string Name { get; protected set; }
        public string Server { get; protected set; }
        public string Database { get; protected set; }
        public int Type { get; protected set; }

        public AuditStoreDatabase(AuditStoreDatabaseList.Output output)
        {
            Id = output.Id;
            AuditStoreId = output.AuditStoreId;
            Guid = output.Guid;
            Name = output.Name;
            Server = output.Server;
            Database = output.Database;
            Type = output.Type;
        }

        public AuditStoreDatabase(AuditStoreDatabaseListAll.Output output)
        {
            Id = output.Id;
            AuditStoreId = output.AuditStoreId;
            Guid = output.Guid;
            Name = output.Name;
            Server = output.Server;
            Database = output.Database;
            Type = output.Type;
        }
    }

    class SessionMetaData
    {
        public bool Exists { get; protected set; }
        public Guid DatabaseGuid { get; protected set; }
        public bool IsHeadRecord { get; protected set; }
        public DateTime? EndTime { get; protected set; }
        public int? MinSequence { get; protected set; }
        public int? MaxSequence { get; protected set; }
        public long? MinTimeStamp { get; protected set; }
        public long? MaxTimeStamp { get; protected set; }
        public bool AgentConnected { get; protected set; }

        public AuditStoreDatabase Database { get; protected set; }

        public SessionMetaData(AuditStoreDatabase database)
        {
            Database = database;
        }
    }

    class WindowsSessionMetaData : SessionMetaData
    {
        public WindowsSessionMetaData(AuditStoreDatabase database, WindowsSessionGetMetadata.Output output)
            : base(database)
        {
            Exists = output.Exists;
            DatabaseGuid = output.DatabaseGuid;
            IsHeadRecord = output.IsHeadRecord;
            EndTime = output.EndTime;
            MinSequence = output.MinSequence;
            MaxSequence = output.MaxSequence;
            MinTimeStamp = output.MinTimeStamp;
            MaxTimeStamp = output.MaxTimeStamp;
            AgentConnected = output.AgentConnected;
        }
    }

    class UnixSessionMetaData : SessionMetaData
    {
        public UnixSessionMetaData(AuditStoreDatabase database, UnixSessionGetMetadata.Output output)
            : base(database)
        {
            Exists = output.Exists;
            DatabaseGuid = output.DatabaseGuid;
            IsHeadRecord = output.IsHeadRecord;
            EndTime = output.EndTime;
            MinSequence = output.MinSequence;
            MaxSequence = output.MaxSequence;
            MinTimeStamp = output.MinTimeStamp;
            MaxTimeStamp = output.MaxTimeStamp;
            AgentConnected = output.AgentConnected;
        }
    }

    public class DatabaseHelper2
    {
        private static List<AuditStore> m_auditStores;
        private static List<AuditStoreDatabase> m_auditStoreDatabases;

        private static List<SessionMetaData> m_sortedMetadatas;
        private static List<Guid> m_databaseHistoryIds; //find the auditstore database which was write the recode.
        private static IEnumerable<SessionMetaData> m_sortedWithDataMetadataList;

        public static List<AuditStore> AuditStores
        {
            get
            {
                return
                    (from s in m_auditStores//get all the auditStores in DB
                     join d in m_auditStoreDatabases
                        on s.Id equals d.AuditStoreId
                     select s).Distinct().ToList();
            }
        }

        public static List<int> AuditStoreDatabaseIds
        {
            get { return m_sortedWithDataMetadataList.Select(metaData => metaData.Database.Id).ToList(); }
        }

        public static List<Guid> AuditStoreDatabaseHistoryIds
        {
            get { return m_databaseHistoryIds; }
        }

        public static PlayerType PlayerType { get; private set; }

        public static SessionDataStatus SessionDataStatus { get; private set; }

        #region Initialize

        public static void Initialize(string connectionString, int auditStoreId, Guid sessionId)
        {
            SessionDataStatus = SessionDataStatus.DataNotFound;
            m_databaseHistoryIds = GetDatabaseHistoryIds(connectionString, auditStoreId);
            GetAuditStoreDatabases(connectionString, auditStoreId, sessionId);

            if (SessionDataStatus == SessionDataStatus.NoAuditStoreDatabaseAvailable ||
                SessionDataStatus == SessionDataStatus.DataNotFound ||
                SessionDataStatus == SessionDataStatus.NoDataIsCapturedForThisSessionYet)
            {
                return;
            }

            GetAuditStores(connectionString);
        }

        private static List<Guid> GetDatabaseHistoryIds(string connectionString, int auditStoreId)
        {
            IEnumerable<AuditStoreActiveDatabaseHistoryList.Output> outputs =
                AuditStoreActiveDatabaseHistoryList.Execute(connectionString,
                                                             new AuditStoreActiveDatabaseHistoryList.Input(
                                                                         auditStoreId));
            //var sortList = outputs.OrderBy(output => output.ActiveTime);
            //List<Guid> list = sortList.Select(a => a.Guid).ToList();
            return outputs.OrderBy(output => output.ActiveTime).Select(output => output.Guid).ToList();
        }

        private static void GetAuditStoreDatabases(string connectionString, int auditStoreId, Guid sessionId)
        {
            #region  Get AuditStore database
            if (auditStoreId < 0)
            {
                // Get all databases
                m_auditStoreDatabases = AuditStoreDatabaseListAll.Execute(connectionString, new AuditStoreDatabaseListAll.Input()).
                    Select(database => new AuditStoreDatabase(database)).ToList();
            }
            else
            {
                // Get databases from the specified Audit Store only
                m_auditStoreDatabases = AuditStoreDatabaseList.Execute(connectionString, new AuditStoreDatabaseList.Input(auditStoreId)).
                    Select(database => new AuditStoreDatabase(database)).ToList();
            }

            if (m_auditStoreDatabases.Count < 1)
            {
                SessionDataStatus = SessionDataStatus.NoAuditStoreDatabaseAvailable;
                return;
            }
            #endregion

            #region Get Session Player Type by zone field in auditstoredatabase
            // Probe each of the databases and see if there is any data for the specified session.
            PlayerType = PlayerType.Unknown;
            foreach (var database in m_auditStoreDatabases)
            {
                try
                {
                    SessionGetSummary.Output output = SessionGetSummary.ExecuteNonQuery(connectionString,
                        new SessionGetSummary.Input(sessionId, database.Id));

                    if (output.Zone != Guid.Empty)
                    {
                        PlayerType = PlayerType.UnixPlayer;
                    }
                    else
                    {
                        PlayerType = PlayerType.WindowsPlayer;
                    }

                    break;
                }
                catch (Exception ex)
                {
                    Log.Verbose("Failed to retrieve player type.  Reason: {0}", ex);
                }
            }

            if (PlayerType == PlayerType.Unknown)
            {
                SessionDataStatus = SessionDataStatus.DataNotFound;
                return;
            }
            #endregion

            #region Get Sesson Metadatas
            List<SessionMetaData> sessionMetadatas = new List<SessionMetaData>();

            //filter the databaseIds and get the metadata for current session
            foreach (var database in m_auditStoreDatabases)
            {
                if (PlayerType == PlayerType.WindowsPlayer)
                {
                    //here don't GetMetadata from the V1 database
                    if (database.Type == (int)AuditStoreDatabaseType.Unix) continue;
                    WindowsSessionGetMetadata.Output? winSessionMetadata =
                        GetWinSessionMetaData(connectionString, sessionId, database.Id);
                    if (winSessionMetadata != null)
                        sessionMetadatas.Add(new WindowsSessionMetaData(database, winSessionMetadata.Value));
                }
                else
                {
                    UnixSessionGetMetadata.Output? unixSessionMetadata = GetUnixSessionMetaData(connectionString, sessionId, database.Id);
                    if (unixSessionMetadata != null) sessionMetadatas.Add(new UnixSessionMetaData(database, unixSessionMetadata.Value));
                }
            }

            m_sortedMetadatas = GetSortedMetadatas(sessionMetadatas);
            m_sortedWithDataMetadataList = m_sortedMetadatas.Where(metaData => metaData.MinSequence.HasValue);
            if (m_sortedWithDataMetadataList.Count() < 1)
            {
                SessionDataStatus = SessionDataStatus.NoDataIsCapturedForThisSessionYet;
                return;
            }

            // Session is spanned across multiple Audit Stores.
            if (m_sortedMetadatas.Select(md => md.Database.AuditStoreId).Distinct().Count() > 1)
            {
                SessionDataStatus = SessionDataStatus.MultipleAuditStoreAvailable;
                return;
            }
            #endregion

            SessionDataStatus = DataCheck() ? SessionDataStatus.DataIsCompleted : SessionDataStatus.SomeDataIsMissing;
        }

        private static WindowsSessionGetMetadata.Output? GetWinSessionMetaData(string connectionString, Guid sessionId, int databaseId)
        {
            try
            {
                WindowsSessionGetMetadata.Output winSessionMetadata = WindowsSessionGetMetadata.ExecuteNonQuery(connectionString, new WindowsSessionGetMetadata.Input(sessionId, databaseId));
                if (winSessionMetadata.Exists)
                {
                    return winSessionMetadata;
                }
            }
            catch (System.Data.SqlClient.SqlException ex)
            {
                HandleSqlException(ex);
            }

            return null;
        }

        private static UnixSessionGetMetadata.Output? GetUnixSessionMetaData(string connectionString, Guid sessionId, int databaseId)
        {
            try
            {
                UnixSessionGetMetadata.Output unixSessionMetadata = UnixSessionGetMetadata.ExecuteNonQuery(connectionString, new UnixSessionGetMetadata.Input(sessionId, databaseId));
                if (unixSessionMetadata.Exists)
                {
                    return unixSessionMetadata;
                }
            }
            catch (System.Data.SqlClient.SqlException ex)
            {
                HandleSqlException(ex);
            }

            return null;
        }

        private static List<SessionMetaData> GetSortedMetadatas(List<SessionMetaData> sessionMetadatas)
        {
            List<SessionMetaData> sortedMetadatas = new List<SessionMetaData>();

            // We should sort by sequence because sort by timestamp may be wrong
            // The timestamp may be repeated if we switch audit store or/and we switch collector while collecting data
            // The v1 database timestamp is calculated from captured time
            var orderedMetadatas = sessionMetadatas.OrderBy(a => a.MinSequence);

            var nullMinDateTimeMetadatas = sessionMetadatas.Where(a => a.MinSequence == null);
            if (nullMinDateTimeMetadatas.Count() > 0)
            {
                //When the MinDateTime is null, need to check the IsAgentConnected and EndTime to decide which is last
                var metadatas = orderedMetadatas.Where(a => !a.AgentConnected && a.EndTime == null);
                if (metadatas.Count() > 0)
                {
                    sortedMetadatas.AddRange(metadatas.ToList());
                }

                var lastMetadatas = orderedMetadatas.Where(a => a.AgentConnected || a.EndTime != null);
                if (lastMetadatas.Count() > 0)
                {
                    sortedMetadatas.AddRange(lastMetadatas);
                }
            }
            else
            {
                sortedMetadatas = orderedMetadatas.ToList();
            }

            return sortedMetadatas;
        }

        private static bool DataCheck()
        {
            //int i = 0;
            int? previousMaxSequence = null;
            bool? existsFirstSequence = null;
            bool result = false;

            for (int i = 0; i < m_sortedMetadatas.Count && result; i++)
            {
                SessionMetaData metaData = m_sortedMetadatas[i];
                if (i == 0)
                {
                    result = CheckIsFirst(metaData, ref existsFirstSequence, ref previousMaxSequence);
                }
                else if (i == m_sortedMetadatas.Count - 1)
                {
                    result = CheckIsLast(metaData);
                }
                else
                {
                    result = CheckMiddle(metaData, ref existsFirstSequence, ref previousMaxSequence);
                }
            }
            return result;
        }

        /// <summary>
        /// Check whether exists first Auditor Store DB(Is head record)
        /// </summary>
        /// <param name="metaData"></param>
        /// <param name="existsFirstSequence"></param>
        /// <param name="previousMaxSequence"></param>
        /// <returns></returns>
        private static bool CheckIsFirst(SessionMetaData metaData, ref bool? existsFirstSequence, ref int? previousMaxSequence)
        {
            if (!metaData.IsHeadRecord)
            {
                return false;
            }

            //Check whether the first DB exists first sequence
            CheckExistsFirstSequence(metaData, ref existsFirstSequence);
            if (existsFirstSequence.HasValue && !existsFirstSequence.Value)
            {
                return false;
            }

            if (metaData.MaxSequence.HasValue)
            {
                previousMaxSequence = metaData.MaxSequence;
            }

            return true;
        }
        
        /// <summary>        
        /// Using session's meta data sequence to check whether the session exists first frame, if not exist then check the next.
        /// </summary>
        /// <param name="metaData"></param>
        /// <param name="existsFirstSequence">When it finds the first frame data, the parameter will be true; false otherwise(the parameter no changes when metaData's MinSequence not have value)</param>
        private static void CheckExistsFirstSequence(SessionMetaData metaData, ref bool? existsFirstSequence)
        {
            if (existsFirstSequence.HasValue && existsFirstSequence.Value)
            {
                return;
            }

            if (metaData.MinSequence.HasValue)
            {
                if (metaData is UnixSessionMetaData)
                {
                    existsFirstSequence = metaData.MinSequence == 1;
                }
                else if (metaData is WindowsSessionMetaData)
                {
                    existsFirstSequence = metaData.MinSequence == 0;
                }
                else
                {
                    Debug.Assert(false, "unknown meta data type");
                }
            }
        }

        /// <summary>
        /// Check whether previous DB's sequence and next DB's sequence is continuous
        /// </summary>
        /// <param name="metaData"></param>
        /// <param name="existsFirstSequence"></param>
        /// <param name="previousMaxSequence"></param>
        /// <returns></returns>
        private static bool CheckMiddle(SessionMetaData metaData, ref bool? existsFirstSequence, ref int? previousMaxSequence)
        {
            //Because may previous DB's not exists frame data, So need check whether the next is first.
            CheckExistsFirstSequence(metaData, ref existsFirstSequence);
            if (existsFirstSequence.HasValue && !existsFirstSequence.Value) return false;

            //Check whether it's sequence is continuous
            if (metaData.MinSequence.HasValue && previousMaxSequence.HasValue)
            {
                if (metaData.MinSequence.Value != previousMaxSequence.Value + 1)
                {
                    return false;
                }
            }

            if (metaData.MaxSequence.HasValue)
            {
                previousMaxSequence = metaData.MaxSequence;
            }

            return true;
        }

        /// <summary>
        /// Check whether the Auditor Store DB is last
        /// </summary>
        /// <param name="metaData"></param>
        /// <returns></returns>
        private static bool CheckIsLast(SessionMetaData metaData)
        {
            /* 
            If there is no Metadata marked as "Agent Connected" or with Session.EndTime, it needs to find out whether this means
            1. the Audit Store Database containing the end part is detached; or
            2. the session is actually being disconnected.
                        
            It can determine this by steps:
            1) Look up for the next database GUID from the Active Database History.
            2) Check whether this is now attached/detached, by querying AuditStoreDatabase table (table AuditStoreDatabase will replace AuditDatabase)
            3) If detached, it means case 1); otherwise 2).
            */

            if (!metaData.AgentConnected && metaData.EndTime == null)
            {
                //because when the collector is paused, we cannot get the "AgentConnected" state from the audit store database.
                //so need check:
                //1. the Audit Store Database containing the end part is detached; or
                //2. the session is actually being disconnected.
                var isLast = CheckIsSessionDisconnected(metaData.DatabaseGuid);
                return isLast;
            }

            return true;
        }

        /// <summary>
        /// check the Audit Store Database containing the end part is detached or the session is actually being disconnected.
        /// </summary>
        /// <param name="databaseGuid"></param>
        /// <returns></returns>
        private static bool CheckIsSessionDisconnected(Guid databaseGuid)
        {
            var indexOfHistory = m_databaseHistoryIds.IndexOf(databaseGuid);
            if (indexOfHistory >= 0 && indexOfHistory + 1 < m_databaseHistoryIds.Count)
            {
                //if can find the database in active history list then check the next database whether in active history list
                var nextDatabaseGuid = m_databaseHistoryIds[indexOfHistory + 1]; //get the next database guid from history list                
                if (m_auditStoreDatabases.FindIndex(database => database.Guid == nextDatabaseGuid) < 0)
                {
                    //if cannot found the next database in AuditStoreDatabase
                    //1. the Audit Store Database containing the end part is detached
                    return false;
                }
                else
                {
                    //can found the next database in AuditStoreDatabase
                    //2. the session is actually being disconnected.
                    return true;
                }
            }
            else
            {
                //if cannot find the database in active history list
                //2. the session is actually being disconnected.
                return true;
            }
        }

        public static void GetAuditStores(string connectionString)
        {
            m_auditStores = AuditStoreList.Execute(connectionString, new AuditStoreList.Input())
                .Select(output => new AuditStore(output)).ToList();
        }

        #endregion

        #region GetDatabaseIdsForUnixSession
        public static List<int> GetDatabaseIdsForUnixSession(int? from, int? to, long? beginTimeStamp)
        {
            List<int> returnValue = new List<int>();
            foreach (var metaData in m_sortedWithDataMetadataList)
            {
                int? minSequence = metaData.MinSequence;
                int? maxSequence = metaData.MaxSequence;
                long? minT = metaData.MinTimeStamp;
                long? maxT = metaData.MaxTimeStamp;

                if (from != null && to != null)
                {
                    if (maxSequence != null && from > maxSequence) continue;
                    if (minSequence != null && minSequence > to) continue;
                }
                else if (from == null && to != null)
                {
                    if (minSequence != null && minSequence > to) continue;
                }
                else if (from != null)
                {
                    if (maxSequence != null && from > maxSequence) continue;
                }

                if (beginTimeStamp != null)
                {
                    if (beginTimeStamp > maxT || minT > beginTimeStamp) continue;
                }

                returnValue.Add(metaData.Database.Id);
            }

            //if cannot get the range return all
            if (returnValue.Count < 1) returnValue = AuditStoreDatabaseIds;

            return returnValue;
        }

        public static List<int> GetDatabaseIdsForUnixSession(int? from, int? to)
        {
            return GetDatabaseIdsForUnixSession(from, to, null);
        }

        public static List<int> GetDatabaseIdsForUnixSession(int? to)
        {
            return GetDatabaseIdsForUnixSession(null, to, null);
        }

        public static List<int> GetDatabaseIdsForUnixSession(long? timeStamp)
        {
            return GetDatabaseIdsForUnixSession(null, null, timeStamp);
        }
        #endregion

        public static int GetStartDatabaseIdForWindowsSession(long startTimeStamp)
        {
            int i = 0;
            foreach (var metaData in m_sortedWithDataMetadataList)
            {
                if (metaData.MinTimeStamp != null && metaData.MaxTimeStamp != null)
                {
                    if (startTimeStamp >= metaData.MinTimeStamp && startTimeStamp <= metaData.MaxTimeStamp)
                        return i;
                }
                else if (metaData.MinTimeStamp == null && metaData.MaxTimeStamp != null)
                {
                    if (startTimeStamp < metaData.MaxTimeStamp)
                        return i;
                }
                else if (metaData.MinTimeStamp != null)
                {
                    if (startTimeStamp > metaData.MinTimeStamp && metaData.AgentConnected)
                        return i;
                }

                i++;
                continue;
            }

            //set the first db is default
            return 0;
        }

        private static void HandleSqlException(System.Data.SqlClient.SqlException ex)
        {
            Log.Verbose("{0}", ex);
            //ExceptionHandler.HandleSqlException(ex);
        }
    }
}
